소셜 로그인 마지막 단계에서의 리디렉션

README

본 문서는 운영진이 여러분의 코드를 리뷰할 때 요청해주신 피드백 타입을 중점적으로 확인할 수 있도록 구체적인 피드백 요구사항을 작성하는 곳입니다.

피드백 타입 (하나 이상의 타입을 선택해주세요)

달성하려는 목표

백엔드 redirect uri로 로그인 완료시 단순 JSON을 넘겨주었더니 웹 브라우저에서 JSON이 뜹니다. 프론트엔드가 정의한 라우트로 이동과 동시에 장고 서버의 액세스 토큰과 리프레시 토큰을 쿼리 파라메터가 아닌 방식으로 전달하고 싶습니다.

내가 작성한 것

코드블럭으로 제공해주셔도 좋고, GitHub 파일 혹은 line URL을 첨부하셔도 됩니다.

오류 메시지

FIG 1.

FIG 1.

답변

https://developers.kakao.com/docs/latest/ko/kakaologin/common#link-and-signup 의 연결과 서비스 가입 항목을 보면 사용자와 앱의 연결 과정이 순서도로 나타나 있습니다.

FIG 2.

FIG 2.

1 ~ 7까지는 제가 지난 특강 (유튜브 링크)를 통해 진행방법을 알려드렸으나, 8번과 관련한 내용은 제공해드리지 않았습니다.

8. 로그인된 서비스 제공

서비스 서버는 카카오로부터 사용자 정보를 식별하여 가입 및 로그인 처리 (7번 참고)까지 완료한 상태입니다. 따라서 서비스 서버는 User 객체를 가지고 있을 것이고, User 정보를 다시 사용자에게 건네주어야 합니다. 사용자 인가 및 특정 기능에 대한 권한부여를 위하여 서비스 서버는 세션 방식의 인가 또는 토큰 방식의 인가를 사용할 것입니다. (참고: https://www.youtube.com/watch?v=UBUNrFtufWo) 이번에는 토큰 방식의 인가에 대해서만 설명하겠습니다.

토큰 방식의 인가

토큰방식은 토큰을 만든 자(예: Django 서버) 가 비밀 키를 사용하여 토큰 페이로드(데이터)를 암호화하여 유저를 식별하는 방식으로, 유저는 마치 놀이동산의 팔찌를 착용한 것과 같이 놀이기구를 타기 전 직원에게 팔찌를 보여주기만 하면 되는 방식을 의미합니다. 따라서 서비스 서버는 로그인 유저정보를 따로 저장할 필요가 없어 성능이 높아집니다. 대표적으로 JWT 토큰을 사용하며, 아래와 같이 두개의 마침표 .로 세 파트가 구분되어있는 모습을 볼 수 있습니다.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

서비스 서버는 대표적으로 jwt라는 패키지 또는 simple-jwt라는 패키지를 사용하여 아주 쉽게 토큰을 생성하고 검증할 수 있습니다. 따라서 7번단계인 “가입 및 로그인 처리”까지 완료된 상황에서 서비스 서버는 User 객체 중에서 민감하지 않은 정보만 페이로드에 담아서 JWT 토큰을 만들어낼 수 있습니다.

def kakao_callback(request):
		...
		user = User.objects.get_or_create(...)
		payload = {
        'user_id': user['id'],
        'username': user['username'],
        'email': user['email'],
        'exp': datetime.utcnow() + timedelta(days=1),
        'iat': datetime.utcnow(),
		}
    jwt_token = jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256')
	  ...

하지만 이때 브라우저의 상태는 소셜 로그인 페이지에서 멈춰있기 때문에 JSON 타입으로 응답할 경우, 브라우저에 JSON 데이터가 그대로 노출이 되는 문제가 발생합니다. (FIG 1. 참조) 이 문제는 서비스 서버가 사용자 페이지로 리다이렉트 하는 것으로 해결할 수 있습니다. 이때 생성한 JWT 토큰은 HTTP 헤더에 담아서 보낼 수 있습니다. 주로 Set-Cookie 헤더를 통하여 삽입할 수 있습니다.

def kakao_callback(request):
		...
    jwt_token = jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256')
    
    # Set the JWT token in a cookie
    response = redirect('your_frontend_url')  # Replace with your frontend URL
    response.set_cookie(
	      'jwt_token', 
	      jwt_token, 
	      httponly=True, 
	      secure=True, 
	      samesite='Lax'
	  )

    return response
	  ...

아래는 소셜 로그인 콜백 핸들러 예시의 전문입니다.

import requests
from django.shortcuts import redirect
from django.http import JsonResponse
from django.conf import settings
import jwt
from datetime import datetime, timedelta

def kakao_callback(request):
    # Get the authorization code from the request
    code = request.GET.get('code')
    
    # Exchange the authorization code for an access token
    token_url = "<https://kauth.kakao.com/oauth/token>"
    payload = {
        'grant_type': 'authorization_code',
        'client_id': settings.KAKAO_CLIENT_ID,
        'redirect_uri': settings.KAKAO_REDIRECT_URI,
        'code': code,
    }
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
    }
    response = requests.post(token_url, data=payload, headers=headers)
    token_json = response.json()
    access_token = token_json.get('access_token')

    # Use the access token to get user information from Kakao
    user_info_url = "<https://kapi.kakao.com/v2/user/me>"
    headers = {
        'Authorization': f'Bearer {access_token}',
    }
    response = requests.get(user_info_url, headers=headers)
    user_info = response.json()

    # Create a JWT token for your application
    payload = {
        'user_id': user_info['id'],
        'exp': datetime.utcnow() + timedelta(days=1),
        'iat': datetime.utcnow(),
    }
    jwt_token = jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256')

    # Set the JWT token in a cookie
    response = redirect('your_frontend_url')  # Replace with your frontend URL
    response.set_cookie('jwt_token', jwt_token, httponly=True, secure=True, samesite='Lax')

    return response

Sequence Diagram

  1. 백엔드 서버로부터 소셜 로그인 URL을 GET 요청으로 받아옵니다. 백엔드 서버는 client_id와 redirect_uri와 같은 query params과 함께 302 redirect 응답을 반환합니다. 이때 redirect_uri는 백엔드의 API 입니다.
  2. 클라이언트는 소셜 로그인 페이지로 이동하여 로그인을 진행합니다.
  3. 로그인이 성공할 경우, OAuth 서버는 백엔드의 redirect_uri로 인가코드와 함께 GET 요청을 보냅니다. 백엔드는 인가코드를 가지고 OAuth 서버로부터 토큰을 검증 및 유저 정보를 획득할 수 있습니다.
  4. 유저 정보를 획득한 백엔드는 데이터베이스에 유저 존재 여부를 검사할 수 있고, 유저가 없다면 회원가입을, 있다면 로그인 로직을 수행하면 됩니다.
  5. 마지막으로 백엔드는 프론트엔드가 정의한 위치로 302 redirect 응답을 반환하여 로그인 시에 끊어졌던 웹 브라우저의 상태를 복원합니다. 백엔드는 클라이언트에게 인가를 위한 JWT 토큰을 발급 할 수 있고, Set-Cookie 헤더에 토큰을 담아 클라이언트가 저장하도록 할 수 있습니다.
sequenceDiagram
    participant Client
	participant OAuth Server
    participant Backend
    participant Database

    Client->>Backend: GET /social-login-url
    Backend-->>Client: 302 Redirect with client_id and redirect_uri

    Client->>OAuth Server: Access OAuth login page

    OAuth Server->>Backend: GET /redirect_uri with Authorization Code
    Backend->>OAuth Server: Validate Authorization Code and Request Tokens

    OAuth Server-->>Backend: Return Tokens and User Info
    Backend->>Database: Check if user exists
    Database-->>Backend: Return User Status (Exists/Not Exists)

    alt User Exists
        Backend->>Backend: Perform Login Logic
    else User Does Not Exist
        Backend->>Backend: Perform Registration Logic
    end

    Backend-->>Client: 302 Redirect to frontend URL with JWT Token in Set-Cookie header